﻿using System;
using System.Collections.Generic;
using T3.Core.Animation;
using T3.Core.Audio;
using T3.Core.Logging;

namespace T3.Editor.Gui.Interaction.Timing
{
    /// <summary>
    /// A helper to provide continuous a BeatTime for live situations.
    /// The timing can be driven by BPM detection or beat tapping.
    ///
    /// Its result is used by <see cref="BeatTimingPlayback"/> to drive playback. 
    /// </summary>
    public static class BeatTiming
    {
        public static double BeatTime { get; private set; }
        public static float Bpm => (float)(60f / _beatDuration);

        public static void SetBpmRate(float bpm)
        {
            _beatDuration = 60f / bpm;
        }

        public static void TriggerSyncTap() => _tapTriggeredLastFrame = true;
        public static void TriggerResyncMeasure() => _syncMeasureTriggeredLastFrame = true;

        public static void Update()
        {
            var runTime = Playback.RunTimeInSecs;
            var distanceToMeasure = 1 - (float)Math.Abs((BeatTime % 4) / 4 - 0.5) * 2;
            BeatTimingDetails.DistanceToMeasure = distanceToMeasure;

            var distanceToBar = 1 - (float)Math.Abs((BeatTime % 1) / 1 - 0.5) * 2;
            BeatTimingDetails.DistanceToBeat = distanceToBar;

            var tappedBeatSync = _tapTriggeredLastFrame;
            _tapTriggeredLastFrame = false;

            var tappedMeasureSync = _syncMeasureTriggeredLastFrame;
            _syncMeasureTriggeredLastFrame = false;

            // Fix BPM if completely out of control
            if (double.IsNaN(Bpm) || Bpm < 20 || Bpm > 200)
            {
                Log.Warning($"BPM {Bpm:0.0} out of range. Reverting to 120");
                _beatDuration = 0.5; // 120bpm
                _phaseDelta = 0;
                _syncMeasureOffset = 0;
                _tapTimes.Clear();
                _resyncTimes.Clear();
            }

            ProcessBeatTaps();
            ProcessMeasureSyncTaps();

            AdvanceBeatTime();
            UpdateDebugData();
            return;

            //--------------------------------------------------------

            void ProcessBeatTaps()
            {
                if (!tappedBeatSync)
                    return;

                BeatTimingDetails.LastTapDistanceToBeat = (float)GetDeltaToSync(runTime);

                AppendRunTimeToQueue(_tapTimes, MaxTapsCount, 2 * MeasureDuration);
                if (_tapTimes.Count < 4)
                {
                    _phaseDelta = 0;
                    return;
                }

                var lastTapTime = _tapTimes[^1];
                if (runTime - lastTapTime > 4)
                {
                    _phaseDelta = 0;
                    _tapTimes.Clear();
                    return;
                }

                var durationSum = 0.0;
                var phaseOffsetSum = 0.0;
                var lastT = 0.0;

                for (var tapIndex = 0; tapIndex < _tapTimes.Count; tapIndex++)
                {
                    var time = _tapTimes[tapIndex];
                    var dt = time - lastT;

                    lastT = time;
                    if (tapIndex == 0)
                        continue;

                    phaseOffsetSum += GetDeltaToSync(time);
                    durationSum += dt;
                }

                var stepCount = _tapTimes.Count - 1;
                _beatDuration = durationSum / stepCount;
                _phaseDelta = phaseOffsetSum / stepCount;
            }

            void ProcessMeasureSyncTaps()
            {
                if (!tappedMeasureSync)
                    return;

                if (distanceToMeasure > 0.1)
                {
                    _resyncTimes.Clear();
                    _resyncTimes.Add(runTime);
                    Log.Debug($"Skipping Sync as tap {distanceToBar:0.00} < {Threshold:0.00}");
                }
                else
                {
                    AppendRunTimeToQueue(_resyncTimes, 8, 8.5 * MeasureDuration);
                    if (_resyncTimes.Count > 1)
                    {
                        var totalMeasureDuration = runTime - _resyncTimes[0];
                        var totalMeasureCount = Math.Round(totalMeasureDuration / MeasureDuration);

                        var originalBpm = Bpm;
                        _beatDuration = totalMeasureDuration / totalMeasureCount / BeatsPerMeasure;
                        Log.Debug($"Refining BPM rate {originalBpm:0.00} -> {Bpm:0.00}.  (  over {totalMeasureDuration:0.00s}s)");
                    }
                }

                _syncMeasureOffset = -(runTime - _measureStartTime) / MeasureDuration;
            }

            void AdvanceBeatTime()
            {
                var timeInMeasure = runTime - _measureStartTime;
                if (timeInMeasure > MeasureDuration)
                {
                    _measureCount++;
                    _measureStartTime += MeasureDuration;
                }

                var tInMeasure = (runTime - _measureStartTime) / MeasureDuration;
                BeatTime = (_measureCount + tInMeasure + _syncMeasureOffset) * BeatsPerBar;
            }

            void UpdateDebugData()
            {
                BeatTimingDetails.WasResyncTriggered = tappedMeasureSync ? 1f : 0f;
                BeatTimingDetails.WasTapTriggered = tappedBeatSync ? 1f : 0f;

                BeatTimingDetails.Bpm = (float)Bpm;
                BeatTimingDetails.SyncMeasureOffset = (float)_syncMeasureOffset;
                BeatTimingDetails.BeatTime = (float)BeatTime;
                BeatTimingDetails.LastPhaseOffset = (float)_phaseDelta;
                BeatTimingDetails.MeasureDuration = (float)MeasureDuration;
            }

            void AppendRunTimeToQueue(IList<double> list, int maxLength, double maxDuration)
            {
                while (list.Count > maxLength || list.Count > 1 && runTime - list[0] > maxDuration)
                {
                    list.RemoveAt(0);
                }

                list.Add(runTime);
            }
        }

        private static double GetDeltaToSync(double time)
        {
            var timeInMeasure = time - _measureStartTime;
            var beatCount = Math.Round(Math.Abs(timeInMeasure) / _beatDuration);
            return timeInMeasure - _beatDuration * beatCount;
        }

        private static double MeasureDuration => _beatDuration * BeatsPerMeasure;
        private const int MaxTapsCount = 16;
        private static readonly List<double> _tapTimes = new(MaxTapsCount + 1);
        private static readonly List<double> _resyncTimes = new(MaxTapsCount + 1);

        private static int BeatsPerMeasure => BeatsPerBar * BarsPerMeasure;
        private static int BarsPerMeasure { get; set; } = 4;
        private static int BeatsPerBar { get; set; } = 4;

        private static double _beatDuration = 1;

        private static double _phaseDelta;
        private static double _measureStartTime;
        private static double _measureCount;
        private static double _syncMeasureOffset;

        private static bool _syncMeasureTriggeredLastFrame;
        private static bool _tapTriggeredLastFrame;

        const double Threshold = 0.3;
    }
}